Skip to content

Conversation

@cpsievert
Copy link
Contributor

@cpsievert cpsievert commented Jan 9, 2026

This PR extends querychat's Python package to support three additional web frameworks alongside Shiny: Streamlit, Gradio, and Dash. Each framework gets a dedicated QueryChat class with a consistent API for integrating natural language data exploration.

Key highlights:

  • New framework-specific modules: querychat.streamlit, querychat.gradio, querychat.dash
  • Unified architecture with shared QueryChatBase class and AppState for state management
  • All frameworks provide .app() for quick start, .ui() for custom layouts
  • Clickable suggestions work across all frameworks
  • Comprehensive documentation with screenshots for each code example
  • Playwright E2E tests covering all frameworks with new CI workflow

Breaking Changes

  • df() return type: Methods like execute_query(), get_data(), and df() now return a narwhals.DataFrame instead of pandas.DataFrame. Call .to_native() to get a pandas DataFrame if needed.
  • Install pandas/polars extras for SQLAlchemy support: pip install querychat[pandas] or pip install querychat[polars]

Architecture

The implementation follows a layered architecture:

┌─────────────────────────────────────────────────────────────┐
│                  Framework-specific classes                 │
│  StreamlitQueryChat │ GradioQueryChat │ DashQueryChat       │
│                     │                 │   + DashUI          │
├─────────────────────┴─────────────────┴─────────────────────┤
│                      QueryChatBase                          │
│  • Data source normalization                                │
│  • System prompt assembly                                   │
│  • Chat client configuration                                │
│  • client(), console(), generate_greeting()                 │
├─────────────────────────────────────────────────────────────┤
│                        AppState                             │
│  • Framework-agnostic session state                         │
│  • Serializable to dict for framework state stores          │
│  • Chat turn management via chatlas                         │
│  • stream_response() / stream_response_async()              │
└─────────────────────────────────────────────────────────────┘

Key design decisions:

  • QueryChatBase centralizes common logic, framework classes inherit and add UI methods
  • AppState can be serialized/deserialized for frameworks with external state stores (Gradio, Dash)
  • Streamlit uses st.session_state directly; Gradio/Dash use gr.State/dcc.Store with serialized dicts
  • Async streaming for Dash (via stream_response_async) to avoid blocking other callbacks

Framework Comparison

Feature Shiny Streamlit Gradio Dash
Quick start .app() .app() .app() .app()
Custom layout .ui()/.sidebar()/.server() .ui()/.sidebar() .ui() .ui() + .init_app()
State access Reactive via .df()/.sql()/.title() .df()/.sql()/.title() .df(state)/.sql(state)/.title(state) .df(state)/.sql(state)/.title(state)
Reset Via chat .reset() method Button in .app() Button in .app()
State storage Shiny reactive st.session_state gr.State (dict) dcc.Store (dict)

Usage Examples

# Streamlit
from querychat.streamlit import QueryChat
qc = QueryChat(df, "titanic")
qc.app()

# Gradio
from querychat.gradio import QueryChat
qc = QueryChat(df, "titanic")
qc.app().launch()

# Dash
from querychat.dash import QueryChat
qc = QueryChat(df, "titanic")
qc.app().run()

Installation

Each framework available as an optional extra:

pip install "querychat[streamlit]"
pip install "querychat[gradio]"
pip install "querychat[dash]"

Files Changed

Core infrastructure (~570 LOC):

  • _querychat_base.py - Shared QueryChatBase class
  • _querychat_core.py - AppState, streaming utilities, type definitions
  • _ui_assets.py - CSS/JS assets for clickable suggestions
  • Renamed _querychat.py_shiny.py, _querychat_module.py_shiny_module.py

Framework integrations (~1130 LOC):

  • _streamlit.py (234 lines) - Session state integration, sidebar/ui methods
  • _gradio.py (396 lines) - gr.State integration, GradioBlocksWrapper for 6.0+ compatibility
  • _dash.py (504 lines) - dcc.Store integration, async callbacks, dash-ag-grid for data display
  • _dash_ui.py (226 lines) - Dash UI component builders

Testing (~1500+ LOC):

  • Playwright e2e tests: test_04_streamlit_apps.py, test_05_gradio_apps.py, test_06_dash_apps.py
  • Unit tests: test_base.py, test_state.py, test_frameworks.py
  • New GitHub Actions workflow: .github/workflows/py-e2e-test.yml

Documentation:

  • build-intro.qmd - Framework-agnostic introduction
  • build-streamlit.qmd, build-gradio.qmd, build-dash.qmd - Framework guides
  • 15 new screenshots demonstrating example apps

For Reviewers

Key files to focus on:

  1. _querychat_base.py - Core shared logic
  2. _querychat_core.py - State management architecture
  3. Framework implementations - Each follows similar patterns

Testing locally:

# Run all Python checks
make py-check

# Run Playwright e2e tests (requires setup first)
make py-e2e-setup
make py-e2e-tests

# Preview docs
make py-docs-preview

# Try individual examples
uv run streamlit run pkg-py/examples/04-streamlit-app.py
uv run python pkg-py/examples/05-gradio-app.py
uv run python pkg-py/examples/06-dash-app.py

Test Plan

  • Unit tests pass (make py-check-tests)
  • Type checks pass (make py-check-types)
  • Linting passes (make py-check-format)
  • Playwright e2e tests pass (make py-e2e-tests)
  • Manual testing of example apps
  • Documentation renders correctly (make py-docs-preview)

Generated with Claude Code

@cpsievert cpsievert force-pushed the feat/py-web-frameworks branch 3 times, most recently from 020bdc8 to 78d46ef Compare January 14, 2026 00:19
cpsievert and others added 4 commits January 13, 2026 18:27
Add support for building querychat applications with popular Python web
frameworks beyond Shiny:

- Streamlit: `querychat.streamlit.QueryChat` with `.app()` and `.sidebar()`
- Gradio: `querychat.gradio.QueryChat` with `.app()` and `.ui()`
- Dash: `querychat.dash.QueryChat` with `.app()`, `.ui()`, and `.init_app()`

Architecture changes:
- Extract shared logic into `_querychat_base.py` and `_querychat_core.py`
- Add `StateDictAccessorMixin` for state deserialization across frameworks
- Modularize Dash UI into `_dash_ui.py` with reusable components
- Use AG Grid instead of DataTable for better table display
- Add CSS/JS assets for each framework's suggestion handling

Includes example apps for each framework demonstrating basic and custom usage.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add end-to-end tests using Playwright to verify:
- Streamlit chat submission and data filtering
- Gradio chat interactions
- Dash callback functionality

Includes CI workflow for running e2e tests on pull requests.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add comprehensive documentation for using querychat with Streamlit,
Gradio, and Dash:

- Framework-specific guides with code examples and screenshots
- Refactored examples to external files with include shortcodes
- Updated navigation and index page
- Added screenshots for all framework examples

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add --ignore=pkg-py/tests/playwright to pytest command in Makefile
  (playwright tests run separately in e2e workflow)
- Add dummy OPENAI_API_KEY fixture to test_frameworks.py
  (tests don't call API but chatlas client requires key on init)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add parallel test execution with pytest-xdist for ~6x speedup
- Add preset greetings to example apps for deterministic test behavior
- Run apps in-process (threads) except Streamlit (subprocess)
- Simplify test configuration: all tests require OPENAI_API_KEY
- Use pypi environment in CI for API key access
- Fix Makefile help regex to show e2e targets

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Fix regex operator precedence bug in SQL_SURVIVED_FILTER pattern
- Refactor _start_server_with_retry to use factory pattern for fresh ports on retries
- Add SO_REUSEADDR to _find_free_port() to mitigate TOCTOU race condition
- Add pytest-rerunfailures for flaky E2E test handling (--reruns 2)
- Add stream_response edge case tests (empty, single chunk, exceptions)
- Add negative/boundary tests for normalize_client and normalize_data_source
- Expand test_frameworks.py coverage for Dash and Streamlit
- Add comprehensive docstrings to app loader helper functions
- Document test isolation strategy and fixture organization

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Fix resource leak in _start_server_with_retry: now cleans up threads,
  processes, and servers on failed startup attempts
- Fix subprocess pipe deadlock: use DEVNULL instead of PIPE for Streamlit
- Simplify verbose docstrings and remove section divider comments

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Remove dead code: delete unused state_to_ui function from _gradio.py
- Consolidate GREETING_PROMPT constant in _querychat_core.py
- Replace max_display_rows parameter with warn_if_large_dataframe()
  - Removes row truncation, adds warning for datasets >10k rows
  - Applied consistently across all four frameworks
- Make state_holder callbacks fail loudly instead of silently skipping
- Document API differences across frameworks in build-intro.qmd
- Simplify test app loaders by removing fragile qc fallback

Co-Authored-By: Claude Opus 4.5 <[email protected]>

This comment was marked as resolved.

@cpsievert cpsievert marked this pull request as ready for review January 14, 2026 21:29
@cpsievert cpsievert requested a review from gadenbuie January 14, 2026 21:29
Remove the large dataframe warning functionality to defer for a follow-up
issue. The eventual implementation will use a max_rows parameter on
.app() to limit displayed data and provide better user feedback.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link
Contributor

@gadenbuie gadenbuie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broad strokes: this looks great! I didn't review all the code in detail, but the architectural design choices seem sounds and you've nicely re-organized the querychat internals to be re-usable across so many backends. Nice work!

Rename *_suggestion.js files to *.js (dash.js, gradio.js, streamlit.js)
and corresponding variables (DASH_JS, GRADIO_JS, STREAMLIT_JS) to allow
for future non-suggestion JS in these files.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@cpsievert cpsievert merged commit 82396ad into main Jan 15, 2026
7 checks passed
@cpsievert cpsievert deleted the feat/py-web-frameworks branch January 15, 2026 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants